今日我們透過解題,來練習看看如何活用property
。以下會以prop
來稱呼由property
建立的property
instance
。
Color
class
:
color
,其為一代表(r, g, b)
格式的tuple
。color
的prop
,具有getter
及setter
功能。hex
的prop
,可動態計算prop
color
的Hex color code
,並具備快取機制。class
Red
繼承Color
:
color
的prop
,其只有getter
功能,而沒有setter
功能。首先定義Color
class
,並實作__init__
。
self.color = color
暗示其會呼叫color
prop
的setter
,來幫忙設定color
。self._hex
為hex
prop
底下真正包含的值,先於此定義。# 01
class Color:
def __init__(self, color):
self.color = color
self._hex = None
接著實作color
prop
的getter
及setter
。
getter
會返回self._color
這個color
prop
底下真正包含的值。setter
會先呼叫_validate
做兩個檢查,確保給定的color
值為tuple
型態且tuple
中每個元素都是範圍在0~255
之間的int
。如果通過的話,則將給定的color
指定給self._color
,並將self._hex
設為None
。self._hex = None
相當於每次呼叫color
的setter
時會清除hex
prop
的快取機制。# 01
class Color:
...
def _validate(self, color):
if not isinstance(color, tuple):
raise ValueError('color value must be a tuple')
if not all(isinstance(c, int) and 0 <= c <= 255 for c in color):
raise ValueError(
'color value must be an integer and 0 <= color value <=255')
@property
def color(self):
return self._color
@color.setter
def color(self, color):
self._validate(color)
self._color = color
self._hex = None # purge cache
再來實作hex
prop
。先確認self._hex
是否為None
,若是的話代表第一次呼叫或是快取已被清除,此時需真正計算Hex color code
,最後再返回self._hex
。請注意我們使用self.color
來存取self._color
,而非直接使用self._color
。雖然兩種方式都可以,但是當有property
這種公開的interface
時,建議優先使用。因為這麼一來當interface
有問題時,身為第一個使用者的我們很容易發現。
# 01
class Color:
...
@property
def hex(self):
if self._hex is None:
self._hex = ''.join(f'{c:02x}' for c in self.color)
return self._hex
最後實作Red
class
。因為我們很明確知道紅色的color
為(255, 0, 0)
,所以可以直接在__init__
中利用super()
來請Color
class
來幫忙設定。
# 01
class Red(Color):
def __init__(self):
super().__init__((255, 0, 0))
但是這麼寫有個問題是,我們可以使用color
prop
的setter
來直接指定color
,像是下面這樣就變成「紅皮綠底」了啊。
>>> red = Red()
>>> red.color = (0, 255, 0) # green
一個解決的方法是,於Red
中實作自己的color
prop
。
class Red(Color):
...
@property
def color(self):
return self.color
可是這麼寫也不行呀,self.color
會不斷再呼叫self.color
直到報錯。此時我們真正需要的是當呼叫self.color
時,利用super()
將呼叫delegate
回Color
,所以應該寫為:
class Red(Color):
...
@property
def color(self):
return super().color
至此我們完成Red
class
的實作。但當試圖建立red
時,卻發現會raise AttributeError
,這是怎麼一回事呢?
>>> red = Red() # AttributeError: property 'color' of 'Red' object has no setter
原來是因為我們在Red
中已經overwrite
了color
prop
,其只有具備getter
功能。而在Red
的__init__
將呼叫delegate
回Color
的__init__
時,其中的self.color = color
會需要呼叫color
prop
的setter
。
請注意這邊有一個在使用property
時常見的誤解。有些朋友可能會覺得我們於Red
中只overwrite
了color
的getter
,但是沒有overwrite
的color
的setter
,所以不應該報錯呀?
這個誤解的來源是將getter
與setter
分別視為了兩個prop
。但是正確的思路是,Color
class
其內的color
prop
同時實作有fget
及fset
,而Red
class
其內的color
prop
只有實作fget
。而當我們由red
來呼叫color
時,self
相當於red
,而self.color = color
相當於red.color = color
,由於該prop
沒有實作fset
,所以會報錯。
我們將於解法2
解決這個問題。
我們將原先color
prop
中setter
的邏輯,移到新的_set_color
function
中。並將Color.__init__
中的self.color = color
改為self._set_color(color)
。這麼一來就能符合題意,巧妙的解決問題。
# 02
class Color:
def __init__(self, color):
self._set_color(color)
self._hex = None
def _validate(self, color):
if not isinstance(color, tuple):
raise ValueError('color value must be a tuple')
if not all(isinstance(c, int) and 0 <= c <= 255 for c in color):
raise ValueError(
'color value must be an integer and 0 <= color value <=255')
def _set_color(self, color):
self._validate(color)
self._color = color
self._hex = None # purge cache
@property
def color(self):
return self._color
@color.setter
def color(self, color):
self._set_color(color)
@property
def hex(self):
if self._hex is None:
self._hex = ''.join(f'{c:02x}' for c in self.color)
return self._hex
此外,可以善用Enum
來幫助我們枚舉各種顏色,假設我們現在需要建立Red
、Green
及Blue
三個class
時,可以這麼寫:
# 02
from enum import Enum
...
class MyColor(Enum):
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
class Red(Color):
def __init__(self):
super().__init__(MyColor.RED.value)
@property
def color(self):
return super().color
class Green(Color):
def __init__(self):
super().__init__(MyColor.GREEN.value)
@property
def color(self):
return super().color
class Blue(Color):
def __init__(self):
super().__init__(MyColor.BLUE.value)
@property
def color(self):
return super().color
由於解法2
裡我們於繼承Color
的class
內都要實作color
prop
,我們開始思考是不是能把這個prop
抽象出來。
我們嘗試建立一個ReadColorOnly
class
來包住這個prop
。這麼一來後續的class
繼承ReadColorOnly
及Color
後,只需要實作__init__
即可。
如果只需要建立Red
、Green
及Blue
三個class
時,我們會傾向解法2
。但如果後續有很多像LuckyColor
的class
需要建立時,或許解法3
會是比較好的選擇。
# 03
import random
...
class ReadColorOnly:
@property
def color(self):
return super().color
class Red(ReadColorOnly, Color):
def __init__(self):
super().__init__(MyColor.RED.value)
class Green(ReadColorOnly, Color):
def __init__(self):
super().__init__(MyColor.GREEN.value)
class Blue(ReadColorOnly, Color):
def __init__(self):
super().__init__(MyColor.BLUE.value)
class LuckyColor(ReadColorOnly, Color):
def __init__(self):
super().__init__(tuple(random.choices(range(256), k=3)))
本日內容啟發自python-deepdive-Part 4-Section 06-Single Inheritance-Delegating to Parent。